package com.fdq.dianping.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpStatus;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fdq.dianping.entity.Result;
import com.fdq.dianping.entity.Shop;
import com.fdq.dianping.mapper.ShopMapper;
import com.fdq.dianping.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fdq.dianping.util.Constants;
import com.fdq.dianping.util.RedisClient;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.fdq.dianping.util.Constants.*;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author fdq
 * @since 2024-12-29
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedisClient redisClient;

    @Override
    public Result queryById(Long id, HttpServletResponse response) {

        Shop shop = null;
        //解决缓存穿透
        //Shop shop = queryWithPassThrough(id,response);

        //解决缓存击穿（同时解决了穿透）
        try {
            shop = queryWithMutex(id);
        } catch (InterruptedException e) {
            response.setStatus(HttpStatus.HTTP_INTERNAL_ERROR);
            throw new RuntimeException(e);
        }

        return Result.success(shop);

    }


    /*
    * 解决缓存穿透问题（多个线程同时访问一个不存在数据库中的数据导致数据库压力大，解决办法是给数据库中不存在的数据在redis中设置一个空值）
    * */
    public Shop queryWithPassThrough(Long id,HttpServletResponse response){
        //1.从redis查询商户缓存
        Map<Object, Object> shopMap_fromRedis = stringRedisTemplate.opsForHash().entries(Shop_Prefix + id);

        //用于返回的对象
        Shop shop = new Shop();
        //2.命中直接返回
        if(!shopMap_fromRedis.isEmpty()){
            BeanUtil.fillBeanWithMap(shopMap_fromRedis,shop,true);
            return shop;
        }

        //3.未命中从数据库中查
        QueryWrapper<Shop> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("id", id);
        shop = getOne(queryWrapper);
        //4.未查到返回404，同时在redis中创建一个空的值，这样下次访问的时候就不用访问数据库了(解决缓存穿透）
        if(shop == null){
            response.setStatus(HttpStatus.HTTP_NOT_FOUND);
            Shop shop_Gen = new Shop();
            stringRedisTemplate.opsForHash().putAll(Shop_Prefix+id,BeanUtil.beanToMap(shop_Gen));
            stringRedisTemplate.expire(Shop_Prefix+id,Constants.Shop_Null_TTL,TimeUnit.MINUTES);
            return shop_Gen;
        }
        //5.查到写入redis
        Map<String,Object> shopMap_setRedis = BeanUtil.beanToMap(shop);
        //把map<String,Object>变成map<String,String>
        Map<String, String> shopMap_setRedis_String = new HashMap<>();
        for (Map.Entry<String, Object> entry : shopMap_setRedis.entrySet()) {
            // 将值转换为字符串
            shopMap_setRedis_String.put(entry.getKey(), entry.getValue().toString());
        }
        stringRedisTemplate.opsForHash().putAll(Shop_Prefix+id,shopMap_setRedis_String);
        //设置有效时间
        stringRedisTemplate.expire(Shop_Prefix+id, Shop_TTL, TimeUnit.MINUTES);
        return shop;
    }


    /*
    * 解决缓存击穿问题（重建缓存时，多个线程都想访问数据库中的数据导致压力增大，解决办法是设置锁）
    * */
    public Shop queryWithMutex(Long id) throws InterruptedException {
        //1.从redis查询商户缓存
        Map<Object, Object> shopMap_fromRedis = stringRedisTemplate.opsForHash().entries(Shop_Prefix + id);

        //用于返回的对象
        Shop shop = new Shop();
        //2.命中直接返回
        if(!shopMap_fromRedis.isEmpty()){
            BeanUtil.fillBeanWithMap(shopMap_fromRedis,shop,true);
            return shop;
        }

        //未命中尝试获取锁
        String mutexKey = Constants.Shop_Mutex_prefix+id;
        while (!tryLock(mutexKey)){
            //未获取到锁，休眠一段时间
            Thread.sleep(Constants.Shop_Mutex_TTL);
        }
        //获取到锁：1.第一个线程，2.其他等了一段时间后的线程获得了锁
        //判断是否命中
        try {
            shopMap_fromRedis = stringRedisTemplate.opsForHash().entries(Shop_Prefix + id);
            //若此时命中直接返回
            if(!shopMap_fromRedis.isEmpty()){
                BeanUtil.fillBeanWithMap(shopMap_fromRedis,shop,true);
                return shop;
            }
            Thread.sleep(100);
            //3.未命中从数据库中查
            QueryWrapper<Shop> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("id", id);
            shop = getOne(queryWrapper);
            //4.未查到在redis中创建一个空的值，这样下次访问的时候就不用访问数据库了(解决缓存穿透）
            if(shop == null){
                Shop shop_Gen = new Shop();
                stringRedisTemplate.opsForHash().putAll(Shop_Prefix+id,BeanUtil.beanToMap(shop_Gen));
                stringRedisTemplate.expire(Shop_Prefix+id,Constants.Shop_Null_TTL,TimeUnit.MINUTES);
                return shop_Gen;
            }
            //5.查到写入redis
            Map<String,Object> shopMap_setRedis = BeanUtil.beanToMap(shop);
            //把map<String,Object>变成map<String,String>
            Map<String, String> shopMap_setRedis_String = new HashMap<>();
            for (Map.Entry<String, Object> entry : shopMap_setRedis.entrySet()) {
                // 将值转换为字符串
                //可能有的属性为空，为空就写入空值
                if(entry.getValue() == null){
                    shopMap_setRedis_String.put(entry.getKey(), "");
                }else {
                    shopMap_setRedis_String.put(entry.getKey(), entry.getValue().toString());
                }
            }
            stringRedisTemplate.opsForHash().putAll(Shop_Prefix+id,shopMap_setRedis_String);
            //设置有效时间
            stringRedisTemplate.expire(Shop_Prefix+id, Shop_TTL, TimeUnit.MINUTES);
            return shop;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //最终要释放锁
            unLock(mutexKey);
        }

    }



    private boolean tryLock(String key){
        //该方法创建的键值对如果该键已存在则返回0，改键不存在返回1
        //可以利用此原理实现互斥锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, Constants.Shop_Mutex, 2, TimeUnit.SECONDS);
        //flag是包装类可能为null，需要转为boolean数据类型
        return BooleanUtil.isTrue(flag);
    }

    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }


    @Override
    @Transactional      //保证事务执行的原子性
    public Result update(Shop shop) {
        System.out.println(shop);
        Long id = shop.getId();
        if(id == null){
            return Result.error("商户id不能为空");
        }
        //1.先操作数据库
        boolean updateSuccess = updateById(shop);
        if(!updateSuccess){
            return Result.error("修改商户信息失败");
        }
        //2.再删除redis
        stringRedisTemplate.delete(Shop_Prefix+id);
        return Result.success("商户信息修改成功");
    }

    @Override
    public Result queryByName(String name) {

        List<Shop> shops = redisClient.queryListWithPassThrough(Shop_Prefix, name,
                Shop.class, name2 -> getByName(name2), Shop_TTL, TimeUnit.MINUTES);

        return Result.success(shops);
    }

    private List<Shop> getByName(String name){
        QueryWrapper<Shop> queryWrapper = new QueryWrapper<>();
        queryWrapper.like("name", name);
        return list(queryWrapper);
    }


    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //是否按距离查询
        if(x == null || y == null){
            //按评分排序
            //查询redis、按照评分排序、分页
            if (current != 1) {
                // 只有第一页使用逻辑过期缓存，其他页走数据库
                Page<Shop> page = query().eq("type_id", typeId)
                        .orderByDesc("score")
                        .page(new Page<>(current, Page_Size));
                return Result.success(page);
            }
            RedisClient redisClient = new RedisClient(stringRedisTemplate);
            List<Shop> cachedList = redisClient.queryWithLogicalExpire(
                    Shop_Score_Key, Shop_Score_Mutex_Prefix,
                    typeId, List.class,
                    // 数据库回查函数
                    id -> query().eq("type_id", id).orderByDesc("score").list(),
                    10L, TimeUnit.MINUTES
            );

            if (cachedList == null) {
                //数据预热
                Page<Shop> page = query().eq("type_id", typeId)
                        .orderByDesc("score")
                        .page(new Page<>(1, Page_Size));
                redisClient.setWithLogicalExpire(Shop_Score_Key, page, 10L, TimeUnit.MINUTES);
                return Result.success(page);
            }

            // 包装成分页对象（这里只缓存第一页，直接包装）
            Page<Shop> page = new Page<>();
            page.setRecords(cachedList);
            page.setCurrent(1);
            page.setSize(Page_Size);
            return Result.success(page);
        }

        // 计算分页参数
        int from = (current - 1) * Page_Size;
        int end = current * Page_Size;

        //查询redis、按照距离排序、分页
        String key = Shop_Geo_Key + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = null;
        results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
                .search(
                        key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(Shop_distance),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)      //查找包含距离，从第一个找到第end个数据
                );
        //缓存为空则从数据库中写入缓存
        if (results.getContent().isEmpty()) {
            // 1.查询店铺信息
            List<Shop> list = list();
            // 2.把店铺分组，按照typeId分组，typeId一致的放到一个集合
            Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
            // 3.分批完成写入Redis
            for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
                // 3.1.获取类型id   将map中每个key对应的商户写入redis
                Long typeIdCreate = entry.getKey();
                String keyCreate = Shop_Geo_Key + typeIdCreate;
                // 3.2.获取同类型的店铺的集合
                List<Shop> value = entry.getValue();
                //locations用于存放每个商铺的坐标信息与id，全部存放好后一次性写入redis
                List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());
                // 3.3.写入redis GEOADD key 经度 纬度 member
                for (Shop shop : value) {
                    // stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString());
                    locations.add(new RedisGeoCommands.GeoLocation<>(
                            shop.getId().toString(),
                            new Point(shop.getX(), shop.getY())
                    ));
                }
                stringRedisTemplate.opsForGeo().add(keyCreate, locations);
                stringRedisTemplate.expire(keyCreate, Shop_TTL, TimeUnit.MINUTES);
            }
        }

        //写入后再查一次
        results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
                .search(
                        key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(Shop_distance),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)      //查找包含距离，从第一个找到第end个数据
                );
        //还为空说明没有该种类
        if(results.getContent().isEmpty()){
            return Result.error("没有该商户种类:"+key);
        }

        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        if (list.size() <= from) {
            // 没有下一页了，结束
            return Result.success(null);
        }
        // 4.1.截取 from ~ end的部分
        //用于存放所有查到的id
        List<Long> ids = new ArrayList<>(list.size());
        //id与距离对应
        Map<String, Distance> distanceMap = new HashMap<>(list.size());
        list.stream().skip(from).forEach(result -> {
            // 4.2.获取店铺id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            // 4.3.获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr, distance);
        });
        // 5.根据id查询Shop
        String idStr = StrUtil.join(",", ids);
        //查找出id在ids中的元素并且按id的顺序排序
        List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
        for (Shop shop : shops) {
            //给查找出的元素赋值距离
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        // 6.返回
        return Result.success(shops);

    }

    @Override
    public Result queryShopByDistance(Integer current, Double x, Double y) {
        // 计算分页参数
        int from = (current - 1) * Page_Size;
        int end = current * Page_Size;

        //查询redis、按照距离排序、分页
        String key = Shop_Geo_Key;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = null;
        results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
                .search(
                        key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(Shop_distance),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)      //查找包含距离，从第一个找到第end个数据
                );

        //缓存为空则从数据库中写入缓存
        if (results.getContent().isEmpty()) {
            // 1.查询店铺信息
            List<Shop> shops = list();
            //locations用于存放每个商铺的坐标信息与id， 全部存放好后一次性写入redis
            List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(shops.size());
            // 3.3.写入redis GEOADD key 经度 纬度 member
            for (Shop shop : shops) {
                locations.add(new RedisGeoCommands.GeoLocation<>(
                        shop.getId().toString(),
                        new Point(shop.getX(), shop.getY())
                ));
            }
            stringRedisTemplate.opsForGeo().add(key, locations);
            stringRedisTemplate.expire(key, Shop_TTL, TimeUnit.MINUTES);
        }

        //写入后再查一次
        results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
                .search(
                        key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(Shop_distance),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)      //查找包含距离，从第一个找到第end个数据
                );
        //还为空说明没有该种类
        if(results.getContent().isEmpty()){
            return Result.success("没有商户");
        }

        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        if (list.size() <= from) {
            // 没有下一页了，结束
            return Result.success(null);
        }
        // 4.1.截取 from ~ end的部分
        //用于存放所有查到的id
        List<Long> ids = new ArrayList<>(list.size());
        //id与距离对应
        Map<String, Distance> distanceMap = new HashMap<>(list.size());
        list.stream().skip(from).forEach(result -> {
            // 4.2.获取店铺id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            // 4.3.获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr, distance);
        });
        // 5.根据id查询Shop
        String idStr = StrUtil.join(",", ids);
        //查找出id在ids中的元素并且按id的顺序排序
        List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
        for (Shop shop : shops) {
            //给查找出的元素赋值距离
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        // 6.返回
        return Result.success(shops);
    }

}
